<?php
/****************************************************************************************
*   Class   :   firewall
*   Ver.    :   1.0
*   Author  :   Temperini Mirko  <dottwatson@hotmail.it>
*   Date    :   03-29-2010
*   License :   GPL License
*
*   IMPORTANT!!!
*
*    The firewall must be EVER started at the top of  the script ( think to session_start() as condition rules)  
*
*   ONLY ONE ISTANCE IS ALLOWED IN A PAGE!!! 
*   
* 
*   calling costructor, you can pass directly the rules table to parse
*   eg: 
*   
*   $myFirewall = new firewall(fielname.ext);
*   and the table is directly loaded
*
*
*
*   you can set 2 different types of output on deny ip:
*   default:     send an header with 403 Status (Forbidden)
*   redirect:    it redirect by a single javascript line to a link specified.
*               why javascript? because when a server is not propelry setted or the page have already sended the header, it not fail.  
*               $myFirewall->setAction('redirect','http://www.google.com');
*
*   show:       simply output the code you pass in 
*               $myFirewall->setAction( 'show',file_get_contents('forbidden.tpl') );
*
*
*   if no action is specified, the default is a blank page whit 403 Status Header (Forbidden)
*
*
*   ok, we turn On our firewall
*   $myFirewall->start();
*   
*   ----------------------------------------
*   |CAUTION!                               |
*   ----------------------------------------------------------------------------------------------------
*   the forceHostname() method try to resolve an IP from ah hipotetic hostname, when it isn't a valid   |
*   this feature can make your firewall slow! Depends from the necessary time to resolve the IP!        |
*   ----------------------------------------------------------------------------------------------------   
*   
*this class is successfull tested on PHP 5.3 
*
*   please report bugs at : <dottwatson@hotmail.it>
*   
*
*
*
*THANKS TO:
*
* Asbjorn Grandt,   <http://www.phpclasses.org/browse/author/193800.html> 
*                   for his contribute for better organize the rules evalution 
*
*
*
*CHANGELOG:
*
*04/22/2010 fixed same aspect of rules rexep
*           added internal trigger error                ($this->trace on $this->errors[] and access to array 
*                                                        by $class->getErrors()) 
*                                                       parsing errors of rules are logged too
*           added ability to dinamically load rules     ($class->importRule('allow 12.34.56.78'))
*           extended arguments for callback functions   ($clientIp,$matched_rule,$errors[] )
*              
****************************************************************************************/

class firewall{
    private $rules_table    =""; //path to rules table
    private $code_rules     =array('allow'=>'','deny'=>'');
    private $action         ="";
    private $source         ="";
    private $ipClient       =null;
    private $force_hostname =false  ;
    private $onDenied       ='';
    private $onAllowed      ='';
    private $errors         =array();
    private $matched_rule   ="";    
    
    final public function __construct($rules_table=""){
        $this->rules_table=$rules_table;
        if(trim($rules_table) !=""){
            if(is_file($rules_table) && is_readable($rules_table)){
                $this->loadTable($this->rules_table);
                }
            else{
                $this->trace("WARNING: unable to load $rules_table.\n");
                }    
            }
        }
    
    final public function start(){
        $this->ipClient     =($this->ipClient === null)?$this->clientIp():$this->ipClient;
        
        if( !$this->isAllowed($this->ipClient) ){
            switch($this->action){
                case 'redirect':
                    echo '<script type="text/javascript">location.href="'.$this->source.'"</script>';
                break;
                case 'show':
                    echo $this->source;
                break;
                default:
                    header('HTTP/1.1 403 Forbidden');
                break;
                }
            if($this->onDenied != '')
                call_user_func($this->onDenied,$this->ipClient,$this->matched_rule,$this->errors);
            exit();
            }
        else{
            if($this->onAllowed != '')
                call_user_func($this->onAllowed,$this->ipClient,$this->matched_rule,$this->errors);
            }
        }
    
    final private function storeRule($type,$values,$ruleStr){
        $this->ipClient =($this->ipClient === null)?$this->clientIp():$this->ipClient;
        $clientIp       =$this->ip2int($this->ipClient);
        $action         =$values[1];
        
        $code_to_eval="if(__MATCH__){
            \$rule_finded=\"{$ruleStr}\";
            return true;
            } \n";

        switch($type){
            case 'range':
                $from   =$this->ip2int($values[2]);
                $to     =$this->ip2int($values[3]);

                $tmp    =$from;
                $from   =($from < $to)  ?$from  :$to;
                $to     =($tmp < $to)   ?$to    :$tmp;
                $this->code_rules[$action]=str_replace('__MATCH__',"$clientIp >= $from  && $clientIp <= $to",$code_to_eval);
            break;
            case 'simple':
                $ip=$this->ip2int($values[2]);
                $this->code_rules[$action]=str_replace('__MATCH__',"$clientIp == $ip",$code_to_eval);
            break;
            }
        }
    

    
    final public function isAllowed($ip){
        $this->matched_rule="";
        $rules=$this->code_rules;       
        // first check allowed arrays. if not results, check denied array. if not results return true;
        if(!empty($rules['allow'])) {
            if(eval($rules['allow'] . "return false;")) {
                $this->matched_rule=$rule_finded;
                return true;
                }
            }
        
        if(!empty($rules['deny'])) {
            if(eval($rules['deny'] . "return false;")){
                $this->matched_rule=$rule_finded;
                return false;
                } 
            }
        return true;
        }
    
    
    final private function ip2int($ip){
        $tmp=explode('.',$ip);
        foreach($tmp as $t_key=>$t_val) $tmp[$t_key]=str_pad($t_val,3, "0", STR_PAD_LEFT); 
        return implode('',$tmp);
        }
    
    
    final private function isValidIp($ip){
        return preg_match('/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/', $ip);
        }
    
    final private function clientIp(){
            if  (!empty($_SERVER['HTTP_CLIENT_IP']))        $ip=$_SERVER['HTTP_CLIENT_IP'];
        elseif  (!empty($_SERVER['HTTP_X_FORWARDED_FOR']))  $ip=$_SERVER['HTTP_X_FORWARDED_FOR'];
        else                                                $ip=$_SERVER['REMOTE_ADDR'];
        
        return $ip;
        }    
    
    final public function loadTable($rule_table=''){
        $this->rules_table=$rule_table;
        $rulesFile=$this->rules_table;
        
        if(is_file($rulesFile) && is_readable($rulesFile)){
            $rules=file($rulesFile,FILE_IGNORE_NEW_LINES);
            if(!$rules) $rules=array();
            
            if(!empty($rules)){                
                foreach($rules as $ruleLine){
                    $this->importRule($ruleLine);
                    }
                }
            }
        }

    final public function importRule($ruleSting){
        $ruleLine=trim($ruleSting);
        if($ruleLine !=""){
            // check syntax allow/deny n.n.n.n to n.n.n.n
            preg_match('/^(deny|allow)[\s](.*)[\s]to[\s](.*)$/i',$ruleLine,$values);

            if(!empty($values) && $this->isValidIp($values[2]) && $this->isValidIp($values[3])){
                $this->storeRule('range',$values,$ruleLine);
                return;
                }
            //check syntax allow/deny simple n.n.n.n
            preg_match('/^(deny|allow)[\s](.*)$/i',$ruleLine,$values);
            
            if(!empty($values) && !empty($values[2])){
                $checkIp=$this->isValidIp($values[2]);
                if( !(bool)$checkIp ){
                    //try to resolve ip by the given string...
                    if($this->force_hostname) 
                        $checkRemoteIp=gethostbyname($checkIp);
                    
                    $values[2]=(isset($checkRemoteIp) && (bool)$checkRemoteIp !== false )?$checkRemoteIp:$values[2];
                    }
                $this->storeRule('simple',$values,$ruleLine);
                return;
                }

            if(strpos($ruleLine,'#') !== 0) $this->trace('NOTICE: unable to parse "'.$ruleLine.'"'."\n");
            }
        }
    
    
    final public function onAllow($callback=''){
        $this->onAllowed=trim((string)$callback);
        }    

    final public function onDeny($callback=''){
        $this->onDenied=trim((string)$callback);
        }    


    //CAUTION! 
    //this feature can make your firewall slow! Depends from the necessary time to resolve the IP!
    final public function forceHostname($value=true){
        $this->force_hostname=(bool)$value;
        }
    
    final public function setAction($type,$source){
        $this->action=$type;
        $this->source=$source;
        }

    final private function trace($e){
        $this->errors[]=$e;
        }
    final public function getErrors($html=false){
        return ($html == true)?
            str_replace("\n","<br />",implode($this->errors)):
            $this->errors;
        }
    final public function matchedRule(){
        return $this->matched_rule;
        }
    }
